#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Thibauld Favre <tfavre@gmail.com>
# This module is a derived work from the python flickrapi that can
# be found at http://flickrapi.sf.net/
#
# Copyright (c) 2007 by the respective coders, see
# http://flickrapi.sf.net/
#
# This code is subject to the Python licence, as can be read on
# http://www.python.org/download/releases/2.5.2/license/
#
# For those without an internet connection, here is a summary. When this
# summary clashes with the Python licence, the latter will be applied.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import sys
import md5
import urllib
import urllib2
import mimetools
import os.path
import logging
import copy
import string
import time
import request
import smcagent

from smcagent.tokencache import TokenCache, CertificateCache
from smcagent.xmlnode import XMLNode

logging.basicConfig()
LOG = logging.getLogger(__name__)
LOG.setLevel(logging.DEBUG)

class IllegalArgumentException(ValueError): pass
class SMCError(Exception): pass

class SMCApi:
    webHostSSL = "https://secure.savemyconf.com/"
    wsHost = "http://api.savemyconf.com/"
    wsHostSSL = "https://secure.savemyconf.com/api/"
    devRegister = "devregister.php"
    devStatus = "devstatus.php"
    devRequests = "devrequests.php"
    devUpload = "upload.php"

    def __init__(self, apiKey, fail_on_error=True):
        """Construct a new SMCApi instance for a given API key and secret."""
        self.apiKey = apiKey
        self.frob = None
        self.auth_url = None
        self.token_cache = TokenCache(apiKey)
        self.token = self.token_cache.token
        self.secret = self.token_cache.secret
        self.certificate_cache = CertificateCache(apiKey)
        self.certificate = self.certificate_cache.certificate
        self.fail_on_error = fail_on_error
        self.__handlerCache={}

    def __repr__(self):
        return '[SMCApi for key "%s"]' % self.apiKey
    __str__ = __repr__

    def sign(self, dictionary):
        """Calculate the signature for a set of params.
        data -- a hash of all the params and values to be hashed, e.g.
                {"api_key":"AAAA", "auth_token":"TTTT", "key": u"value".encode('utf-8')}
        """
        data = [self.secret]
        keys = dictionary.keys()
        keys.sort()
        for key in keys:
            data.append(key)
            datum = dictionary[key]
            if isinstance(datum, unicode):
                raise IllegalArgumentException("No Unicode allowed, "
                        "argument %s (%r) should have been UTF-8 by now"
                        % (key, datum))
            data.append(datum)
        md5_hash = md5.new()
        md5_hash.update(''.join(data))
        return md5_hash.hexdigest()

    def encode_and_sign(self, dictionary, join=True):
        """URL encodes the data in the dictionary, and signs it using the
        given secret, if a secret was given.
        """
        dictionary = self.make_utf8(dictionary)
        if self.secret:
            dictionary['rq_sig'] = self.sign(dictionary)
        if join: return urllib.urlencode(dictionary)
        return dictionary

    def make_utf8(self, dictionary):
        """Encodes all Unicode strings in the dictionary to UTF-8. Converts
        all other objects to regular strings.
        Returns a copy of the dictionary, doesn't touch the original.
        """
        result = {}
        for (key, value) in dictionary.iteritems():
            if isinstance(value, unicode):
                value = value.encode('utf-8')
            else:
                value = str(value)
            result[key] = value
        return result

    def __getattr__(self, method):
        """Handle all the regular SMC API calls.
        """

        if method.startswith('__'):
            raise AttributeError("No such attribute '%s'" % method)

        if method in self.__handlerCache:
            return self.__handlerCache[method]

        ns, method = string.split(method,'_')
        if ns == 'register':
            ns = SMCApi.devRegister
        elif ns == 'requests':
            ns = SMCApi.devRequests
        elif ns == 'status':
            ns = SMCApi.devStatus
        elif ns == 'upload':
            ns = SMCApi.devUpload
        else:
            raise SMCError("Unknown namespace '%s'" % ns)

        def handler(**args):
            """Dynamically created handler for a SMC API call"""

            # Set some defaults
            rq_method = 'get'
            ssl = False
            plain = False
            headers_cb = None
            if 'headers_cb' in args:
                headers_cb = args['headers_cb']
                del args['headers_cb']

            if 'rq_method' in args:
                rq_method = args['rq_method']
                del args['rq_method']

            if 'ssl' in args:
                ssl = args['ssl']
                del args['ssl']

            if 'plain' in args:
                plain = args['plain']
                del args['plain']

            defaults = {'method': method,
                    'token': self.token,
                    'rq_key': self.apiKey}

            for key, default_value in defaults.iteritems():
                if key not in args:
                    args[key] = default_value
                # You are able to remove a default by assigning None
                if key in args and args[key] is None:
                    del args[key]

            LOG.debug("Calling %s(%s)" % (method, args))
            params = self.encode_and_sign(args)
            if rq_method == 'get':
                if ssl: url=SMCApi.wsHostSSL + ns + '?' + params
                else: url=SMCApi.wsHost + ns + '?' + params
                f = urllib.urlopen(url)
            elif rq_method == 'post':
                if ssl: url=SMCApi.wsHostSSL + ns
                else: url=SMCApi.wsHost + ns
                if ns=='upload': f = self.upload(url,params)
                else: f = urllib.urlopen(url, params)
            else:
                raise SMCError("Unknown request method %s" % rq_method)

            data = f.read()
            f.close()
            if headers_cb:
                headers_cb(self,f.info())
            #LOG.debug("DATA: %s" % data)
            #LOG.debug("HEADERS: %s" % f.info())
            LOG.debug("URL: %s" % url)
            LOG.debug("Params: %s" % params)
            result = data
            if not plain:
                result = XMLNode.parseXML(data, True)
                if self.fail_on_error:
                    SMCApi.testFailure(result, True)
            return result

        self.__handlerCache[method] = handler
        return self.__handlerCache[method]

    def __getAuthURL(self, frob):
        """Return the authorization URL to get a token.
        This is the URL the app will launch a browser toward if it
        needs a new token.
        """
        encoded = self.encode_and_sign({
            "rq_key": self.apiKey,
            "frob": self.frob,
            "method": 'authenticate'})
        return "%s%s?%s" % (SMCApi.webHostSSL, SMCApi.devRegister, encoded)

    @classmethod
    def testFailure(cls, rsp, exception_on_error=True):
        """Exit app if the rsp XMLNode indicates failure."""
        if rsp['stat'] != "error":
            return
        message = cls.getPrintableError(rsp)
        LOG.error(message)
        if exception_on_error:
            raise SMCError(message)

    @classmethod
    def getPrintableError(cls, rsp):
        """Return a printed error message string."""
        return "%s: error %s: %s" % (rsp.elementName, cls.getRspErrorCode(rsp), cls.getRspErrorMsg(rsp))

    @classmethod
    def getRspErrorCode(cls, rsp):
        """Return the error code of a response, or 0 if no error."""
        if rsp['stat'] == "error":
            return rsp.error[0]['code']
        return 0

    @classmethod
    def getRspErrorMsg(cls, rsp):
        """Return the error message of a response, or "Success" if no error."""
        if rsp['stat'] == "error":
            return rsp.error[0].elementText
        return "Success"

    def checkTokenValidity(self):
        """Get a token either from the cache, or make a new one from the
        frob.

        This first attempts to find a token in the user's token cache on
        disk. If that token is present and valid, it is returned by the
        method.
        """
        token = self.token_cache.token
        auth_url = None
        frob = None
        if token:
            LOG.debug("Trying cached token '%s'" % token)
            try:
                rsp = self.requests_check(rq_key=self.apiKey, token=token)
            except SMCError:
                LOG.debug("Cached token invalid")
                del self.token_cache.token
                del self.certificate_cache.certificate
                token = None
                self.token = None
                self.certificate = None
        if not token:
            LOG.debug("Getting frob for new token")
            rsp = self.register_getFrob(rq_key=self.apiKey, token=None)
            self.testFailure(rsp)
            self.frob = rsp.frob[0].elementText
            self.auth_url = self.__getAuthURL(frob)
        return token is not None

    def getToken(self):
        if self.token:
            LOG.debug("getToken: no need, token already there")
            return self.token
        LOG.debug("getToken: trying to get a new token for frob '%s'" % self.frob)
        token = None
        try:
            rsp = self.register_getToken(rq_key=self.apiKey, token=self.frob, ssl=True)
            self.testFailure(rsp)
            token = rsp.token[0].elementText
            self.frob = None
            self.auth_url = None
            LOG.debug("getToken: received new token '%s'" % token)
        except SMCError:
            LOG.debug("getToken: device not yet registered")
        if not token:
            return False 
        self.token_cache.token = rsp.xml
        self.token = token
        self.secret = self.token_cache.secret
        cert = self.requests_getCertificate(ssl=True,plain=True)
        self.certificate_cache.certificate = cert
        self.certificate = self.certificate_cache.certificate
        LOG.debug("getToken: received code certificate '%s'" % cert)
        return True

    def getRequest(self,callback):
        def analyseCodeSig(self,headers):
            info = str(headers);
            begin_idx = string.find(info,"Signed code:") + len("Signed code: ");
            end_idx = begin_idx + 1;
            signed = info[begin_idx:end_idx];
            if signed=="1": self.code_signed = True
            else: self.code_signed = False

        rsp = self.requests_getRequest(ssl=True)
        self.testFailure(rsp)
        req_id = rsp.request[0]['rq_id']
        req_name = rsp.request[0]['name']
        raw_code = self.requests_getCode(rq_id=req_id,ssl=True,plain=True,headers_cb=analyseCodeSig)
        code_url = os.path.expanduser(os.path.join(smcagent.code_cache,'code.zip'))
        if self.code_signed:
            code_zip = open(os.path.expanduser(code_url+'.sig'),'wb')
        else:
            code_zip = open(os.path.expanduser(code_url),'wb')
        code_zip.write(raw_code)
        code_zip.close()
        req_files = {}
        req_params = {}
        for item in rsp.request[0].files[0].file:
            req_files[item.name[0].elementText] = item.url[0].elementText
        for item in rsp.request[0].configuration[0].property:
            req_params[item.name[0].elementText] = item.value[0].elementText
        req = request.Request(self,req_id,req_name,req_params,req_files,code_url,self.code_signed,self.certificate,self.token,callback)
        del self.__dict__['code_signed']
        self.requests_rqAccepted(rq_id=rsp.request[0]['rq_id'],ssl=True,rq_method='post',plain=True)
        return req

    def reqAvailable(self):
        rsp = self.requests_check()
        time.sleep(int(rsp.wait[0].elementText))
        return int(rsp.nb[0].elementText)

    def reqEndStatus(self,id,st): 
        return self.requests_rqFinished(rq_id=id,status=st,ssl=True,rq_method='post',plain=True)

    def devStatus(self,st): 
        return self.status_stUpdate(status=st,ssl=True,rq_method='post')
